package interpeter;

import ic.ast.*;
import ic.ast.decl.ClassType;
import ic.ast.decl.DeclClass;
import ic.ast.decl.DeclField;
import ic.ast.decl.DeclLibraryMethod;
import ic.ast.decl.DeclMethod;
import ic.ast.decl.DeclStaticMethod;
import ic.ast.decl.DeclVirtualMethod;
import ic.ast.decl.Parameter;
import ic.ast.decl.PrimitiveType;
import ic.ast.decl.PrimitiveType.DataType;
import ic.ast.expr.BinaryOp;
import ic.ast.expr.BinaryOp.BinaryOps;
import ic.ast.expr.Length;
import ic.ast.expr.Literal;
import ic.ast.expr.NewArray;
import ic.ast.expr.NewInstance;
import ic.ast.expr.RefArrayElement;
import ic.ast.expr.RefField;
import ic.ast.expr.RefVariable;
import ic.ast.expr.StaticCall;
import ic.ast.expr.This;
import ic.ast.expr.UnaryOp;
import ic.ast.expr.UnaryOp.UnaryOps;
import ic.ast.expr.VirtualCall;
import ic.ast.stmt.LocalVariable;
import ic.ast.stmt.Statement;
import ic.ast.stmt.StmtAssignment;
import ic.ast.stmt.StmtBlock;
import ic.ast.stmt.StmtBreak;
import ic.ast.stmt.StmtCall;
import ic.ast.stmt.StmtContinue;
import ic.ast.stmt.StmtIf;
import ic.ast.stmt.StmtReturn;
import ic.ast.stmt.StmtWhile;


@SuppressWarnings("serial")
public class REPL implements Visitor
{
	State state = new State();
	public static class RuntimeError extends Error {
		public RuntimeError(String msg) { super(msg); }
	}

	public static class BreakException extends RuntimeException {
		public BreakException() {super();}}
	public static class ContinueException extends RuntimeException {
		public ContinueException() {super();}}
	
	@Override
	public Object visit(UnaryOp unaryOp) {
		
		if  (unaryOp.getOperator() == UnaryOps.LNEG){
			return ! (Boolean) unaryOp.getOperand().accept(this);
		}else if (unaryOp.getOperator() == UnaryOp.UnaryOps.UMINUS)
			return - (Integer) unaryOp.getOperand().accept(this);
		return null;
	}

	@Override
	public Object visit(BinaryOp binaryOp) {
		Object a,b;
		a= binaryOp.getFirstOperand().accept(this);
		b= binaryOp.getSecondOperand().accept(this);
		
		if (binaryOp.getOperator() == BinaryOps.PLUS){
			//int or string
			if (a instanceof String || b instanceof String){
				return ((String) a ) + ((String) b );
			}else{ 
				return ((Integer) a ) +	((Integer) b);
			}
		}else if (binaryOp.getOperator() == BinaryOps.MINUS){
			
			return ((Integer) a ) -	((Integer) b);
		
		}else if (binaryOp.getOperator() == BinaryOps.MULTIPLY){
			
			return ((Integer) a ) *	((Integer) b);
		
		}else if (binaryOp.getOperator() == BinaryOps.DIVIDE){
			
			return ((Integer) a ) /	((Integer) b);
		
		}else if (binaryOp.getOperator() == BinaryOps.MOD){
			
			return ((Integer) a ) %	((Integer) b);
		
		}else if (binaryOp.getOperator() == BinaryOps.LAND){
			
			return ((Boolean) a ) && ((Boolean) b);
		
		}else if (binaryOp.getOperator() == BinaryOps.LOR){
			
			return ((Boolean) a ) || ((Boolean) b);
		
		}else if (binaryOp.getOperator() == BinaryOps.LT){
			
			return ((Integer) a ) < ((Integer) b);
		
		}else if (binaryOp.getOperator() == BinaryOps.LTE){
			
			return ((Integer) a ) <= ((Integer) b);
		
		}else if (binaryOp.getOperator() == BinaryOps.GT){
			
			return ((Integer) a ) >	((Integer) b);
		
		}else if (binaryOp.getOperator() == BinaryOps.GTE){
			
			return ((Integer) a) >=	((Integer) b);
		
		}else if (binaryOp.getOperator() == BinaryOps.EQUAL){
			// int or boolean
			if (a instanceof Boolean){
				return ((Boolean) a ) == ((Boolean) b);
			}else 
				return ((Integer) a ) == ((Integer) b);
	
		}else if (binaryOp.getOperator() == BinaryOps.NEQUAL){
			// int or boolean
			if (a instanceof Boolean){
				return ((Boolean) a ) != ((Boolean) b);
			}else 
				return ((Integer) a ) != ((Integer) b);
		}else 
			return null;
	}

	//*****************************ready to go**************************************/
	
	@Override
	public Object visit(DeclVirtualMethod method)  {
		return visitMethod(method);
	}
	@Override
	public Object visit(DeclStaticMethod method)  {
		return visitMethod(method);
	}
	private Object visitMethod(DeclMethod method)  {
		for (int i = 0; i < method.getStatements().size(); i++){	
			// method.getStatements().get(i).accept(this);
			Statement st = method.getStatements().get(i);
			if (st instanceof StmtReturn) {
				Object o = st.accept(this);
				return o;
			}else{
				st.accept(this);
			}
		}
		return null;
	}
	@Override
	public Object visit(StmtReturn returnStatement)  {
		return returnStatement.getValue().accept(this);
	}

	@Override
	public Object visit(StmtIf ifSt)  {
		if ((boolean) ifSt.getCondition().accept(this)){
			return ifSt.getOperation().accept(this);
		}else{
			return ifSt.getElseOperation()!= null ?  ifSt.getElseOperation().accept(this) : null;
		}
	}

	@Override
	public Object visit(StmtWhile whileStatement)  {	
		while ((boolean) whileStatement.getCondition().accept(this)){
			try	{
				whileStatement.getOperation().accept(this);
			}catch (ContinueException e){
				continue;
			}catch (BreakException e) {
				break;
			}	
		}
	return null;
	}

	@Override
	public Object visit(StmtBreak breakStatement)  {
		throw new BreakException();
	}

	@Override
	public Object visit(StmtContinue continueStatement) {
		throw new ContinueException();
	}

	@Override
	public Object visit(StmtBlock statementsBlock)  {
		for (int i=0; i < statementsBlock.getStatements().size(); i++){
			statementsBlock.getStatements().get(i).accept(this);
		}
		return null;
	}
	
	@Override
	public Object visit(NewArray newArray) {
		int arrSize = (Integer) newArray.getSize().accept(this);
		switch ((String)((PrimitiveType) newArray.getType()).accept(this)) {
		case "boolean":
			return new Boolean[arrSize];
		case "int":
			return new Integer[arrSize];
		case "string":
			return new String[arrSize];
		default:
			break;
		} 
		return null;
	}
	
	@Override
	public Object visit(Literal literal) {
		if (literal.getType() == DataType.BOOLEAN){
			
			if (literal.getValue() instanceof String){
				return ((String)literal.getValue()).equals("true");
		
			} else if (literal.getValue() instanceof Boolean){
				return literal.getValue();
			}
			
			return (Boolean) literal.getValue();
		}else if (literal.getType() == DataType.INT){
			
			if (literal.getValue() instanceof String){
				return Integer.parseInt((String) literal.getValue());
		
			} else if (literal.getValue() instanceof Integer){
				return literal.getValue();
			}
		
		}else if (literal.getType() == DataType.STRING){
			return (String) literal.getValue();
		}
	return null;	
	}
	
	@Override
	public Object visit(StmtAssignment assignment) {
		if (assignment.getVariable() instanceof RefVariable){
			
			state.updateVar(((RefVariable)assignment.getVariable()).getName(),
							assignment.getAssignment().accept(this));

		} else if (assignment.getVariable() instanceof RefArrayElement){
			
			RefArrayElement rae = ((RefArrayElement)assignment.getVariable());
			//get array pointer
			Object[] arrPtr= (Object[]) rae.getArray().accept(this) ;
			
			//put the value in proper position
			arrPtr[(Integer)rae.getIndex().accept(this)] = assignment.getAssignment().accept(this);
		}
		return null;
	}

	//int a [= 4]
	@Override
	public Object visit(LocalVariable localVariable)  {
		state.updateVar(localVariable.getName(), 
					 localVariable.getInitialValue() == null ? null : localVariable.getInitialValue().accept(this));
		return null;
	}

	@Override
	public Object visit(PrimitiveType type) {
		return type.getDisplayName();
	}
	@Override
	public Object visit(Length length) {
		RefArrayElement rae = ((RefArrayElement)length.getArray() );
		//get array pointer
		Object[] arrPtr= (Object[]) rae.getArray().accept(this) ;		
		return arrPtr.length;
	}
	
	@Override
	public Object visit(RefArrayElement location) {
		//get array pointer
		Object[] arrPtr= (Object[]) location.getArray().accept(this) ;
		
		//return the value in proper position
		return arrPtr[(Integer)location.getIndex().accept(this)];
	}
	
	@Override
	public Object visit(RefVariable location) {
		//return location.getName();
		return state.lookup(location.getName());
	}
	///*********************************************************************/
	
	
	//////////********no need***********///////////
	
	@Override
	public Object visit(RefField location) {
		
		return null;
	}

	
	@Override
	public Object visit(ic.ast.decl.Program program) {
		// no need
		return null;
	}
	@Override
	public Object visit(DeclClass icClass) {
		// no need
		return null;
	}
	@Override
	public Object visit(DeclLibraryMethod method) {
		// no need
		return null;
	}
	@Override
	public Object visit(Parameter formal) {
		// no need
		return null;
	}
	@Override
	public Object visit(StmtCall callStatement) {
		// no need
		return null;
	}
	@Override
	public Object visit(StaticCall call) {
		// no need
		return null;
	}

	@Override
	public Object visit(VirtualCall call) {
		// no need
		return null;
	}
	@Override
	public Object visit(DeclField field) {

		return null;
	}
	
	@Override
	public Object visit(NewInstance newClass) {
	
		return null;
	}

	@Override
	public Object visit(ClassType type) {

		return null;
	}

	@Override
	public Object visit(This thisExpression) {
		// no need
		return null;
	}

}
